#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <stdint.h>
#include <sys/socket.h>
#include <sys/un.h>

#include "cJSON.h"
#include "debugcmd.h"

/* Debug printf */
#define DERR        0   /* error */
#define DWAN        1   /* warning */
#define DINF        2   /* information */
#define DBUG        3   /* debug */
#define DIGN        4   /* ignore */

#define MAZRET_ISHELPCMD    0x40

/**
 * dmsg: debug message
 * dlog: debug log
 * cmsg: console message(with timestamp)
 * clog: console log(with timestamp)
 *
 * @note: 每个模块必须要有 static int dlvl 变量
 */
#define dmsg(lvl, fmt, arg...)  if(lvl <= dlvl) {                                                   \
                                    printf(fmt, ##arg);                                             \
                                }

#define dlog(lvl, fmt, arg...)  if(lvl <= dlvl) {                                                   \
                                    printf("[%04d][%s] "fmt"\r\n", __LINE__, __FUNCTION__, ##arg);  \
                                }

static int dlvl = DINF;

#define HISTORY_COUNT                       (20)
#define HISTORY_PREV(n)                     n = ((n >= HISTORY_COUNT - 1) ? 0 : (n - 1))
#define HISTORY_NEXT(n)                     n = ((n >= HISTORY_COUNT - 1) ? 0 : (n + 1))
uint8_t history_cmd[HISTORY_COUNT][DEBUGCMD_MAX_STR_LEN];
int success_count = 0;                      // 命令执行成功的次数计数
int base = 0;

#define SOCKET_MSG_MAXLEN                   256
#define SOCKET_FILE                         "/tmp/socket_dcmd"
#define JSON_FILE                           "/tmp/cmd.json"

/* 函数声明 */
void __io_putchar(char c);
int dcmd_prompt(void);
int dcmd_logo(void);
int dcmd_register_by_json(void);
int dcmd_register_by_local(void);
void dcmd_process_signal(int signo);
void dcmd_shell_linux_mode(void);
void dcmd_shell_uart_mode(void);
int OutputOpen(void *Cmd, char *pcParam);
int dcmd_is_help_cmd(char *cmd);

/*系统重定向命令,此处添加*/
static DEBUGCMD_MCMD stOutputCmd         = { "output",      "dup debug command." };
static DEBUGCMD_SCMD stOutputOpen        = { "open",         "dup output redirect open.", OutputOpen};
/**
 * @brief 主函数
 */
int main()
{
    int ret = 0;
    char cmd[DEBUGCMD_MAX_STR_LEN];                 // 用户缓存命令的临时字符数组
    uint32_t fill = 0;                              // 串口接收队列中有效数据长度
    uint32_t length = 0;                            // 命令长度
    char vt100[4] = { 0 };                          // 暂存解析到的VT100控制码的值
    char match_cmd[DEBUGCMD_MAX_STR_LEN];
    int match_length = 0;

    int sockfd = 0;
    struct sockaddr_un address;
    uint32_t i;
    ssize_t cnt = 0;
    char retmsg[16] = {0};

    signal(SIGINT, dcmd_process_signal);

    /**
     * offset = 0 表示当前命令
     * offset = 1 表示上一条命令
     * offset = 2 表示上上条命令
     * offset 的取值范围为 [0,HISTORY_COUNT)
     * 数学中方括号表示包含, 小括号表示不包含, 因此取值范围为 0 ~ HISTORY_COUNT-1
     */
    int offset = 0;                                 // 历史命令的相对于当前命令的偏移值

    /* 调试命令组件初始化 */
    debugcmd_init();

    /* 命令注册位置 */
    dcmd_register_by_json();
    /* dcmd本地命令注册位置 */
    dcmd_register_by_local();

    memset(cmd, 0, DEBUGCMD_MAX_STR_LEN);
    memset(match_cmd, 0, DEBUGCMD_MAX_STR_LEN);

    dcmd_shell_uart_mode();
    dcmd_logo();
    dcmd_prompt();

    while (1)
    {
        /* 一个字符一个字符的取出数据 */
        cmd[length] = getchar();

        /* 字符回显, 针对没有输入情况下按退格键和TAB键做特殊处理 */
        if (length == 0 && (cmd[length] == '\b' || cmd[length] == '\t'))
        {
            /* 终端还没有输入字符, 此时按TAB和退格输出提示音 */
            cmd[length] = '\0';
            length = 0;
            __io_putchar('\a');
            continue;
        }
        else if (cmd[length] == '\t')
        {
            /* 终端已经存在部分字符, 按TAB键将尝试做自动补全处理 */
            cmd[length] = '\0';
            ret = debugcmd_automatic_completion(cmd, match_cmd, &match_length);

            /* 根据当前的场景, 自动补全有三种情况 */
            /**
             * 情况1: MAZRET_ENEWLINE -- 有多个字符串匹配, 则补全到字符串差异位置
             * 情况2: MAZRET_EAUTO ----- 只有一个字符串匹配, 则补全这个字符串, 并且追加一个空格
             * 情况3: MAZRET_ERING ----- 没有任何一个字符串匹配, 说明当前输入的内容有误
             */
            if (MAZRET_ENEWLINE == ret)
            {
                /* 打印新的一行命令提示符 */
                dcmd_prompt();

                /* 如果当前输入的内容没有到多个匹配的字符串的差异位置则补全到差异位置 */
                if (match_length)
                {
                    strcat(cmd, match_cmd);
                    length += match_length;
                }

                printf("%s", cmd);
                fflush(stdout);
            }
            else if (MAZRET_EAUTO == ret)
            {
                /* 只有一个命令包含, 则自动补全这个命令 */
                if (match_length)
                {
                    strcat(cmd, match_cmd);
                    length += match_length;
                    printf("%s", match_cmd);
                    fflush(stdout);
                }
            }
            else if (MAZRET_ERING == ret)
            {
                /* 命令输入错误, 输出提示音 */
                printf("\a");
                fflush(stdout);
            }

            continue;
        }
        else if (cmd[length] == '\033')    // VT100 '\033'
        {
            /* 发现是VT100控制码, 则继续取出后面的两个字符 */
            vt100[0] = getchar();
            vt100[1] = getchar();
            if (vt100[0] == '[')
            {
                /* 判断是否是上下键 */
                if (vt100[1] == 'A' || vt100[1] == 'B')
                {
                    int i = 0;
                    int index = 0;

                    /**
                     * 上键处理方式
                     * 需要判断是否已经回溯到了最后一条命令了.
                     * 最后一条命令有两种场景:
                     * (说明: 以MAZAPP_DEBUGCMD_HISTORY_COUNT=8举例,最多缓存8条历史命令)
                     *      场景1: 已经按了7次上键了, 此时 offset 的值为 7, 再按上也不处理了
                     *      场景2: 系统启动后仅输入了2条命令, 因此最多按2次上键, 再按上也不处理了
                     */
                    if ((vt100[1] == 'A') && (offset < (HISTORY_COUNT - 1))
                            && (offset < success_count))
                    {
                        /* 从最新输入行切换到历史命令时, 暂存目前的输入内容*/
                        if (0 == offset)
                        {
                            /* 将最后的 0x1b 换码符清掉, 否则长度对应不上 */
                            cmd[length] = '\0';
                            memset(history_cmd[base], 0, DEBUGCMD_MAX_STR_LEN);
                            memcpy(history_cmd[base], cmd, strlen(cmd));
                        }

                        /* 每次按上键, offset加1 */
                        offset++;

                        /* 根据偏移值计算历史命令的下标 */
                        index = base - offset;

                        /* 由于是环形数组, 因此还需要判断是否回环 */
                        if (index < 0)
                        {
                            index = index + HISTORY_COUNT;
                        }

                        /* 先清除当前的输出, 先退格, 然后输出空格清除, 最后退一格 */
                        for (i = 0; i < length; i++)
                        {
                            printf("\b \b");
                            fflush(stdout);
                        }
                        memset(cmd, 0, DEBUGCMD_MAX_STR_LEN);

                        /* 取出下标对应的历史命令 */
                        length = strlen((char *) history_cmd[index]);
                        memcpy(cmd, history_cmd[index], length);

                        /* 打印历史命令 */
                        printf("\r");
                        dcmd_prompt();
                        printf("%s", cmd);
                        fflush(stdout);
                    }
                    else if ((vt100[1] == 'B') && (offset > 0))
                    {
                        offset--;

                        index = base - offset;
                        if (index < 0)
                        {
                            index = index + HISTORY_COUNT;
                        }

                        for (i = 0; i < length; i++)
                        {
                            printf("\b \b");
                            fflush(stdout);
                        }
                        memset(cmd, 0, DEBUGCMD_MAX_STR_LEN);

                        length = strlen((char *) history_cmd[index]);
                        memcpy(cmd, history_cmd[index], length);

                        printf("\r");
                        dcmd_prompt();
                        printf("%s", cmd);
                        fflush(stdout);
                    }
                    else
                    {
                        /* 已经切换到了记录的最早的历史命令了, 输出提示音 */
                        printf("\a");
                        fflush(stdout);
                    }
                }
            }

            cmd[length] = '\0';
            continue;
        }
        else
        {
            __io_putchar(cmd[length]);
        }

        /* 适配 XShell 串口终端 */
        if (cmd[length] == '\r')
        {
            __io_putchar('\n');
        }

        /* 回车换行键处理 */
        if (cmd[length] == '\r' || cmd[length] == '\n')
        {
            cmd[length] = '\0';

            if(length > 0)
            {
                ret = dcmd_is_help_cmd(cmd);

                /* 如果是帮助命令, 则跳过本地处理过程 */
                if(ret == MAZRET_ISHELPCMD)
                {
                    /* 直接标记为远端处理 */
                    ret = MAZRET_REMOTECMD;
                }
                else
                {
                    /* 本地处理, 找不回调函数则会认为是远端通过json传递过来的命令 */
                    ret = debugcmd_execute(cmd);
                }

                if (MAZRET_REMOTECMD == ret)
                {
                    sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
                    if (-1 == sockfd)
                    {
                        printf("err: socket\n");
                        dcmd_shell_linux_mode();
                        return -1;
                    }

                    /* connect to the server */
                    address.sun_family = AF_UNIX;
                    strcpy(address.sun_path, SOCKET_FILE);
                    ret = connect(sockfd, (struct sockaddr *)&address, sizeof(address));
                    if (-1 == ret)
                    {
                        printf("err: connect\n");
                        dcmd_shell_linux_mode();
                        return -1;
                    }

                    cnt = write(sockfd, &cmd, strlen(cmd));
                    if (0 >= cnt)
                    {
                        printf("err: write\n");
                        dcmd_shell_linux_mode();
                        return -1;
                    }

                    recv(sockfd, retmsg, 16, 0);
                    dlog(DBUG, "%s\n", retmsg);

                    /* close the socket */
                    ret = close(sockfd);
                    if (-1 == ret)
                    {
                        printf("err: close\n");
                        exit(1);
                    }

                    if (!strcmp(retmsg, "OK"))
                    {
                        ret = MAZRET_OK;
                    }
                    else
                    {
                        ret = MAZRET_NG;
                    }
                }
            }
            else
            {
                ret = MAZRET_NG;
            }

            /* 命令执行成功后, 将命令记录到 history 命令列表 */
            if (MAZRET_OK == ret)
            {
                success_count++;
                memset(history_cmd[base], 0, DEBUGCMD_MAX_STR_LEN);
                memcpy(history_cmd[base], cmd, strlen(cmd));
                HISTORY_NEXT(base);
                offset = 0;
            }
            dcmd_prompt();
            memset(cmd, 0, DEBUGCMD_MAX_STR_LEN);
            length = 0;
            continue;
        }

        if (cmd[length] == '\b') /* 退格键处理 */
        {
            __io_putchar(' ');
            __io_putchar('\b');
            cmd[length] = '\0';
            if (length > 0)
            {
                length--;
            }
        }
        else
        {
            length++;
        }

        /* 输入命令长度超过 DEBUGCMD 组件约束 */
        if (length >= DEBUGCMD_MAX_STR_LEN)
        {
            memset(cmd, 0, DEBUGCMD_MAX_STR_LEN);
            dmsg(DERR, "lenght over, length = %u\r\n", length);
            length = 0;
        }
    }

    dcmd_shell_linux_mode();
    return 0;
}

/**
 * @brief 打印单个字符
 * @retval none
 */
void __io_putchar(char c)
{
    printf("%c", c);
    fflush(stdout);
}

/**
 * @brief 信号处理函数
 * @retval none
 */
void dcmd_process_signal(int signo)
{
    printf("\r\n");
    dcmd_shell_linux_mode();
    exit(0);
}

/**
 * @brief 设置终端交互方式为linux模式
 * @retval none
 */
void dcmd_shell_linux_mode(void)
{
    system("stty icanon echo");
}

/**
 * @brief 设置终端交互方式为uart模式
 * @retval none
 */
void dcmd_shell_uart_mode(void)
{
    /**
     * 修改 shell 终端的交互方式
     * -icanon  直接读取, 无需等待enter
     * -echo    不回显
     */
    system("stty -icanon -echo");
}

/**
 * @brief 打印命令提示符
 * @retval 错误码
 */
int dcmd_prompt(void)
{
    printf(">>> ");
    fflush(stdout);
    return MAZRET_OK;
}

/**
 * @brief 打印启动界面
 * @retval 错误码
 */
int dcmd_logo(void)
{
    printf("Welcome to the \"debugcmd\" component!\r\n");
    printf("     _      _                                    _ \r\n");
    printf("  __| | ___| |__  _   _  __ _  ___ _ __ ___   __| |\r\n");
    printf(" / _` |/ _ \\ '_ \\| | | |/ _` |/ __| '_ ` _ \\ / _` |\r\n");
    printf("| (_| |  __/ |_) | |_| | (_| | (__| | | | | | (_| |\r\n");
    printf(" \\__,_|\\___|_.__/ \\__,_|\\__, |\\___|_| |_| |_|\\__,_|\r\n");
    printf("                        |___/                      \r\n");
    printf("\r\n");

    printf("Source  : https://gitee.com/mazcpnt/maz-debugcmd\r\n");
    printf("Document: https://gitee.com/mazcpnt/maz-debugcmd/tree/master/documents\r\n");
    printf("\r\n");

    printf("debugcmd version v%d.%d.%d(%s %s)\r\n",
           DEBUGCMD_MAIN_VER,
           DEBUGCMD_SUB_VER,
           DEBUGCMD_REV_VER,
           __DATE__, __TIME__);
    printf("Try \"%s\" for help information.\r\n", DEBUGCMD_HELP_COMMAND);
    fflush(stdout);
    return MAZRET_OK;
}

/**
 * @brief 解析命令配置文件
 * @retval none
 */
int dcmd_register_by_json(void)
{
    cJSON *json_root;
    cJSON *json_mcmd;
    cJSON *json_scmd;
    char *result;               // 获取到的 JSON 字符串
    int length;                 // 获取到的 JSON 字符串的长度
    char *name = JSON_FILE;     // 输入的 JSON 文件名
    FILE *fp;                   // 输入的 JSON 文件句柄
    int len;                    // 实际读取文件中的数据长度

    DEBUGCMD_MCMD *mcmd = NULL;
    DEBUGCMD_SCMD *scmd = NULL;

    /* 打开JSON文件 */
    fp = fopen(name, "rb");
    if (!fp)
    {
        printf("err: open %s file failed!\r\n", name);
        return -1;
    }

    /* 偏移到文件最后, 查询当前偏移值则为文件大小 */
    fseek(fp, 0L, SEEK_END);
    length = ftell(fp);

    /* 偏移回文件头, 从文件头开始读取数据 */
    fseek(fp, 0L, SEEK_SET);
    result = malloc(length);

    len = fread(result, 1, length, fp);
    if(len != length)
    {
        printf("err: %s read failed! len = %d, length = %d\r\n", name, (int)len, length);
        return -1;
    }

    json_root = cJSON_Parse(result);
    if(NULL == json_root)
    {
        printf("err: %s parse failed\r\n", name);
        return -1;
    }

    json_mcmd = json_root->child;

    while(json_mcmd != NULL)
    {
        dlog(DBUG, "%s\r\n", json_mcmd->string);

        mcmd = (DEBUGCMD_MCMD *)malloc(sizeof(DEBUGCMD_MCMD));
        mcmd->name = json_mcmd->string;
        mcmd->desc = "none";
        mcmd->handler = NULL;
        debugcmd_mcmd_register(mcmd);

        json_scmd = json_mcmd->child;

        while(json_scmd != NULL)
        {
            dlog(DBUG, "    %s\r\n", json_scmd->string);

            scmd = (DEBUGCMD_SCMD *)malloc(sizeof(DEBUGCMD_SCMD));
            scmd->name = json_scmd->string;
            scmd->desc = cJSON_GetObjectItem(json_scmd, "desc")->valuestring;
            scmd->handler = NULL;
            debugcmd_scmd_register(mcmd, scmd);
            json_scmd = json_scmd->next;
        }

        json_mcmd = json_mcmd->next;
    }

    // cJSON_Delete(json_root);

    return 0;
}

/**
 * @brief dcmd进程本地命令注册接口
 * @retval none
 */
int dcmd_register_by_local(void)
{
    int iRet = -1;

    iRet = debugcmd_mcmd_register(&stOutputCmd);
    if(iRet < 0)
    {
        printf("err: maincmd %s register failed. ret = %ul", stOutputCmd.name, iRet);
        return -1;
    }
    iRet = debugcmd_scmd_register(&stOutputCmd, &stOutputOpen);
    if(iRet < 0)
    {
        printf("err: subcmd %s reigster failed. ret = %ul", stOutputOpen.name, iRet);
        return -1;
    }

    return 0;
}

/**
 * @brief 判断是否是 ? 帮助命令
 * @retval none
 */
int dcmd_is_help_cmd(char *cmd)
{
    char *mname = NULL;
    char *param = NULL;
    char tmp[DEBUGCMD_MAX_STR_LEN];

    if(NULL == cmd)
    {
        return MAZRET_EINVAL;
    }

    memset(tmp, 0, DEBUGCMD_MAX_STR_LEN);
    memcpy(tmp, cmd, strlen(cmd));

    mname = strtok_r(tmp, " ", &param);
    if(NULL == mname)
    {
        return MAZRET_EINVAL;
    }

    if(!strcmp(mname, "?"))
    {
        return MAZRET_ISHELPCMD;
    }

    return MAZRET_EINVAL;
}

/*输出重定向*/
int OutputOpen(void *Cmd, char *pcParam)
{
    int ret = 0;
    ssize_t cnt = 0;
    int sockfd = 0;
    char retmsg[16] = {0};
    char *tty_name = NULL;
    struct sockaddr_un address;
    char cmdmsg[DEBUGCMD_MAX_STR_LEN];
    memset(cmdmsg, 0, DEBUGCMD_MAX_STR_LEN);

    /* 获取当前tty名称 */
    tty_name = ttyname(STDOUT_FILENO);
    printf("current tty_name: %s\n", tty_name);
    sprintf(cmdmsg, "app_sys redirect %s", tty_name);
    printf("cmdmsg %s\n", cmdmsg);
    sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (-1 == sockfd)
    {
        printf("err: socket\n");
        dcmd_shell_linux_mode();
        return -1;
    }

    /* connect to the server */
    address.sun_family = AF_UNIX;
    strcpy(address.sun_path, SOCKET_FILE);
    ret = connect(sockfd, (struct sockaddr *)&address, sizeof(address));
    if (-1 == ret)
    {
        printf("err: connect\n");
        dcmd_shell_linux_mode();
        return -1;
    }

    cnt = write(sockfd, &cmdmsg, strlen(cmdmsg));
    if (0 >= cnt)
    {
        printf("err: write\n");
        dcmd_shell_linux_mode();
        return -1;
    }

    recv(sockfd, retmsg, 16, 0);
    dlog(DBUG, "%s\n", retmsg);

    /* close the socket */
    ret = close(sockfd);
    if (-1 == ret)
    {
        printf("err: close\n");
        exit(1);
    }

    if (!strcmp(retmsg, "OK"))
    {
        ret = MAZRET_OK;
    }
    else
    {
        ret = MAZRET_NG;
    }
    return 0;
}
